package org.sean.framework.alibaba.oss;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.Bucket;
import com.aliyun.oss.model.BucketList;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.CreateBucketRequest;
import com.aliyun.oss.model.DeleteObjectsRequest;
import com.aliyun.oss.model.DeleteObjectsResult;
import com.aliyun.oss.model.GenericRequest;
import com.aliyun.oss.model.ListBucketsRequest;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.ObjectAcl;
import com.aliyun.oss.model.ObjectListing;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import com.aliyun.oss.model.SetBucketAclRequest;
import org.sean.framework.code.StatusInfo;
import org.sean.framework.exception.StatusException;
import org.sean.framework.logging.Logger;
import org.sean.framework.util.ThreadUtil;

import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * OSS接口
 *
 * @author xielei
 */
public class OSSClient {

    private Logger logger = Logger.newInstance(OSSClient.class);

    private OSS oss;

    public OSSClient(String endpoint, String accessKeyId, String accessKeySecret) {
        oss = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    }

    /**
     * Create a new OSS bucket if not exist
     *
     * @param bucketName Bucket Name
     * @param control    权限
     */
    public void createBucket(String bucketName, CannedAccessControlList control) {
        run(() -> {
            if (!oss.doesBucketExist(bucketName)) {
                CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
                if (control != null) {
                    createBucketRequest.setCannedACL(control);
                }
                oss.createBucket(createBucketRequest);
            }
            return null;
        });
    }

    /**
     * Checks the {@link Bucket} exists .
     *
     * @param bucketName Bucket name.
     * @return Returns true if the bucket exists and false if not.
     */
    public boolean existBucket(String bucketName) {
        return run(() -> oss.doesBucketExist(bucketName));
    }

    /**
     * Deletes the {@link Bucket} instance. A non-empty bucket could not be
     * deleted.
     *
     * @param bucketName bucket name to delete.
     */
    public void deleteBucket(String bucketName) {
        run(() -> {
            oss.deleteBucket(bucketName);
            return null;
        });
    }

    /**
     * Returns all {@link Bucket} instances of the current account.
     *
     * @return A list of {@link Bucket} instances. If there's no buckets, the
     * list will be empty (instead of null).
     */
    public List<Bucket> listBuckets() {
        return run(() -> oss.listBuckets());
    }


    /**
     * Returns all {@link Bucket} instances of the current account that meet the
     * conditions specified.
     *
     * @param listBucketsRequest the ListBucketsRequest instance that defines the criteria
     *                           which could have requirements on prefix, marker, maxKeys.
     * @return The list of {@link Bucket} instances.
     */
    public BucketList listBuckets(ListBucketsRequest listBucketsRequest) {
        return run(() -> oss.listBuckets(listBucketsRequest));
    }

    /**
     * Upload an object to your bucket
     *
     * @param bucketName buckt
     * @param key        key
     * @param file       文件
     * @return PutObjectResult
     */
    public PutObjectResult putObject(String bucketName, String key, File file) {
        return run(() -> oss.putObject(bucketName, key, file));
    }

    /**
     * Upload an object to your bucket
     *
     * @param putObjectRequest request
     * @return PutObjectResult
     */
    public PutObjectResult putObject(PutObjectRequest putObjectRequest) {
        return run(() -> oss.putObject(putObjectRequest));
    }

    /**
     * Upload an object to your bucket
     *
     * @param bucketName  buckt
     * @param key         key
     * @param inputStream 数据流
     * @return PutObjectResult
     */
    public PutObjectResult putObject(String bucketName, String key, InputStream inputStream) {
        return run(() -> oss.putObject(bucketName, key, inputStream));
    }

    /**
     * Uploads the file from a InputStream instance to the signed URL with
     * specified headers.
     *
     * @param signedUrl        Signed Url, which has the bucket name, object key, account
     *                         information and accessed Ids and its signature. The url is
     *                         recommended to be generated by generatePresignedUrl().
     * @param requestContent   {@link InputStream} instance to read from.
     * @param contentLength    Hint content length to write. if useChunkEncoding is true,
     *                         then -1 is used.
     * @param requestHeaders   Rquest headers,including standard or customized http headers
     *                         documented by PutObject REST API.
     * @param useChunkEncoding The flag of using chunked transfer encoding.
     * @return A {@link PutObjectResult} instance.
     */
    public PutObjectResult putObject(URL signedUrl, InputStream requestContent, long contentLength,
                                     Map<String, String> requestHeaders, boolean useChunkEncoding) {
        return run(() -> oss.putObject(signedUrl, requestContent, contentLength, requestHeaders, useChunkEncoding));
    }

    /**
     * Uploads the file from a specified file path to the signed URL with
     * specified headers with the flag of using chunked tranfer encoding.
     *
     * @param signedUrl        Signed url, which has the bucket name, object key, account
     *                         information and accessed Ids and its signature. The url is
     *                         recommended to be generated by generatePresignedUrl().
     * @param filePath         The file path to read from.
     * @param requestHeaders   Request headers, including standard or customized http headers
     *                         documented by PutObject REST API.
     * @param useChunkEncoding The flag of using chunked transfer encoding.
     * @return A {@link PutObjectResult} instance.
     */
    public PutObjectResult putObject(URL signedUrl, String filePath, Map<String, String> requestHeaders,
                                     boolean useChunkEncoding) {
        return run(() -> oss.putObject(signedUrl, filePath, requestHeaders, useChunkEncoding));
    }

    /**
     * Checks if a specific {@link OSSObject} exists under the specific
     * {@link Bucket}. 302 Redirect or OSS mirroring will not impact the result
     * of this function.
     *
     * @param bucketName Bucket name.
     * @param key        Object Key.
     * @return True if exists; false if not.
     */
    public boolean existObject(String bucketName, String key) {
        return run(() -> oss.doesObjectExist(bucketName, key));
    }

    /**
     * Sends the request to apply ACL on a {@link Bucket} instance.
     *
     * @param bucketAclRequest SetBucketAclRequest instance which specifies the ACL and the
     *                         bucket information.
     */
    public void setObjectAcl(SetBucketAclRequest bucketAclRequest) {
        run(() -> {
            oss.setBucketAcl(bucketAclRequest);
            return null;
        });

    }

    /**
     * Applies the Access Control List(ACL) on the {@link Bucket}.
     *
     * @param bucketName Bucket name.
     * @param acl        {@link CannedAccessControlList} instance. If the instance is
     *                   null, no ACL change on the bucket (but the request is still
     *                   sent).
     */
    public void setObjectAcl(String bucketName, CannedAccessControlList acl) {
        run(() -> {
            oss.setBucketAcl(bucketName, acl);
            return null;
        });

    }

    /**
     * Gets a {@link OSSObject} from {@link Bucket}.
     *
     * @param bucketName Bucket name.
     * @param key        Object Key.
     * @return A {@link OSSObject} instance. The caller is responsible to close
     * the connection after usage.
     */
    public OSSObject getObject(String bucketName, String key) {
        return run(() -> oss.getObject(bucketName, key));
    }

    /**
     * Gets the Access Control List (ACL) of the OSS object.
     *
     * @param bucketName Bucket name.
     * @param key        Object Key.
     * @return The {@link ObjectAcl} instance of the object.
     */
    public ObjectAcl getObjectAcl(String bucketName, String key) {
        return run(() -> oss.getObjectAcl(bucketName, key));
    }


    /**
     * Lists all objects under the specified {@link Bucket} with the specified
     * prefix.
     *
     * @param bucketName Bucket name.
     * @param prefix     The prefix returned object must have.
     * @return A {@link ObjectListing} instance that has all objects
     */
    public ObjectListing listObjects(String bucketName, String prefix) {
        return run(() -> oss.listObjects(bucketName, prefix));
    }

    /**
     * Deletes the specified {@link OSSObject} by bucket name and object key.
     *
     * @param bucketName Bucket name.
     * @param key        Object key.
     */
    public void deleteObject(String bucketName, String key) {
        run(() -> {
                    oss.deleteObject(bucketName, key);
                    return null;
                }
        );
    }

    /**
     * Deletes the specified {@link OSSObject} by the {@link GenericRequest}
     * instance.
     *
     * @param genericRequest The {@link GenericRequest} instance that specfies the bucket
     *                       name and object key.
     */
    public void deleteObject(GenericRequest genericRequest) {
        run(() -> {
                    oss.deleteObject(genericRequest);
                    return null;
                }
        );
    }

    /**
     * Batch deletes the specified files under a specific bucket. If the files
     * are non-exist, the operation will still return successful.
     *
     * @param bucketName           Bucket Name
     * @param deleteObjectsRequest A {@link DeleteObjectsRequest} instance which specifies the
     *                             bucket and file keys to delete.
     * @return A {@link DeleteObjectsResult} instance which specifies each
     * file's result in normal mode or only failed deletions in quite
     * mode. By default it's normal mode.
     */
    public DeleteObjectsResult deleteObjects(String bucketName, DeleteObjectsRequest deleteObjectsRequest) {
        return run(() -> oss.deleteObjects(deleteObjectsRequest));
    }

    /**
     * 生成URL签名
     *
     * @param bucketName Bucket Name
     * @param objectName Object Name
     * @return OSS URL
     */
    public String getOssUrl(String bucketName, String objectName) {
        return run(() -> {
            // 设置URL过期时间为1小时。
            Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
            // 生成以GET方法访问的签名URL，访客可以直接通过浏览器访问相关内容。
            return oss.generatePresignedUrl(bucketName, objectName, expiration).getPath();
        });
    }

    /**
     * Shuts down the OSS instance (release all resources) The OSS instance is
     * not usable after its shutdown() is called.
     */
    public void shutdown() {
        oss.shutdown();
    }

    private <T> T run(Fun<T> fun) {
        try {
            return fun.call();
        } catch (com.aliyun.oss.OSSException e) {
            logger.debug("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            logger.debug("Error Message: " + e.getErrorCode());
            logger.debug("Error Code:       " + e.getErrorCode());
            logger.debug("Request ID:      " + e.getRequestId());
            logger.debug("Host ID:           " + e.getHostId());
            throw new StatusException(StatusInfo.ossError(), e);
        } catch (ClientException e) {
            logger.debug("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            logger.debug("Error Message: " + e.getMessage());
            throw new StatusException(StatusInfo.ossError(), e);
        } finally {
            ThreadUtil.sleep(100);
        }
    }


    /**
     * 执行方法
     *
     * @param <T> 返回类型
     */
    interface Fun<T> {
        T call();
    }

}
